# -*- coding: utf-8 -*-
import requests
import re
import hashlib
import bs4
import sys
import os

from finger import finger
from exp import exp

# 禁用警告，否则requests.get(url, verify=False)的时候会打印警告
requests.packages.urllib3.disable_warnings()

_hashmap = {}
rootdir = os.path.dirname(__file__)
expdir = os.path.join(rootdir, 'exp')

def get(main, sub):
    if (len(main) > 0 and main[-1] != '/') and (len(sub) > 0 and sub[0] != '/'):
        sub = '/' + sub
    url = main + sub
    
    if str(url).find('http') != 0:
        url = 'http://' + url

    url_md5 = hashlib.md5(url.encode()).hexdigest()
    if _hashmap.get(url_md5) is None:
        _hashmap[url_md5] = Get(url)
    return _hashmap[url_md5]

class Get(object):
    def __init__(self, url):
        try:
            self.res = requests.get(url, headers={'User-Agent': 'test'}, verify=False, timeout=5)
            self.hash = hashlib.md5(self.res.content).hexdigest()
            # 检测是否可以渲染text
            content_type = self.res.headers['Content-Type']
            if 'text' in content_type:
                charset = 'utf-8'
                if 'charset' in content_type:
                    charset = re.search(r'charset\=([0-9a-zA-Z\-]+)', content_type).group(1)
                self.text = self.res.content.decode(charset)
            else:
                self.text = ''
            self.error_message = None
        except Exception as e:
            self.res = None
            self.hash = ''
            self.text = ''
            self.error_message = str(e)
        except requests.exceptions.Timeout as e:
            self.res = None
            self.hash = ''
            self.text = ''
            self.error_message = str(e)
            

        self.url = url
        self.paserd_html = None

    def header(self, key):
        if self.res is None:
            return ''
        value = self.res.headers.get(key)
        return '' if value is None else value

    def html(self, selector):
        if self.res is None:
            return ''
        if self.paserd_html is None:
            self.paserd_html = bs4.BeautifulSoup(self.text, 'html.parser')
        return self.paserd_html.select_one(selector).string

def start(fingers, urls):
    result = []
    for i in urls:
        url = i
        index_page = get(url,'')
        # 如果首页访问错误提示错误信息
        if index_page.error_message is not None:
            str = url
            str += ' | ' + index_page.error_message
            print('\033[1;31m[!] ' + str + '\033[0m')
            continue
        g = lambda sub: get(url, sub)
        count = 0
        # 逐个指纹开始匹配
        for j in fingers:
            if j['match'](g):
                # 这里表示匹配指纹成功
                id = j['id']
                version = j['version'](g) if callable(j['version']) else j['version']
                str = url
                str += ' | ' + index_page.html('title')
                str += ' | ' + id
                str += ' ' + version
                server = index_page.header('Server')
                str += ' | ' + (server if server != '' else '--')
                poweredby = index_page.header('X-Powered-By')
                str += ' | ' + (poweredby if poweredby != '' else '--')
                print("\033[1;32m[*] " + str + "\033[0m") #'\033[1;35m字体有色，但无背景色 \033[0m'
                count += 1
                result.append({'url': url, 'id': id, 'version': version})
        # count==0表示什么指纹都没有匹配到
        if count == 0:
            str = url
            str += ' | ' + index_page.html('title')
            str += ' | --'
            server = index_page.header('Server')
            str += ' | ' + (server if server != '' else '--')
            poweredby = index_page.header('X-Powered-By')
            str += ' | ' + (poweredby if poweredby != '' else '--')
            print('[*] ' + str)
    return result

# 执行攻击函数
def attack_it(result):
    for item in result:
        print('[*] (╰_╯) 尝试攻击 ' + item['url'])
        if item['id'] not in exp.keys(): # 判断是否有针对该CMS的exp
            continue
        for ex in exp[item['id']]: # 逐个exp尝试攻击
            if item['version'] == '' or ex['version'] == '' or match_version(item['version'], ex['version']) or match_version(ex['version'], item['version']):
                # 如果没有指定版本或者版本相匹配，就尝试攻击
                exp_path = os.path.join(expdir, item['id'], ex['path'])
                if os.path.exists(exp_path) and os.path.isfile(exp_path):
                    command = 'python %s %s'%(exp_path, item['url'])
                    print('[*] ' + command)
                    os.system(command)

def match_version(regx, version):
    regxs = regx.split(',')
    for item in regxs:
        if item in version:
            return item
        item_reg = item.replace('.', '\\.').replace('x', '[0-9]+')
        result = re.search(item_reg, version)
        if result:
            return result.group()
    return None
# ================================================================================================
urls = []
cms_id = None
cms_version = ''
attack = False
help_str = '''
根据指纹库匹配当前网站使用的是什么系统

使用示例:
    python ahole.py -u 192.168.0.1:81,www.baidu.com
    python ahole.py -u 192.168.0.1:80 --attack
    python ahole.py -u 192.168.0.1:80 --attack -i Discuz -v 2.x

参数说明:
    -u          指定url，可以用逗号指定多个
                每个url尽量加协议头http://或者https://
                url主体可以是ip地址或域名
                非80及443端口需指定端口号

    -l          从本地文件读取url信息，将url写入到文件中，一行一个

    --show-finger    
                查看目前支持识别的CMS

    --attack    尝试攻击

    -i          指定攻击CMS

    -v          指定攻击版本

    -h          查看帮助


添加指纹:
    为了使指纹库识别功能强大，指纹库用的是py文件，即finger.py。
    指纹库需要不断完善，以便识别更多系统

    每个指纹单元的格式为：
        {
            'id': '', #系统名称
            'decription': '', #系统描述
            'version': '' #系统版本，字符串或者返回字符串的lambda表达式
            'match': lambda g: False #匹配条件，返回boolean值的lambda表达式
        }

    lambda表达式参数为g，为经过特殊封装的request.get函数

    例如匹配favicon.ico的hash值：
    lambda g: g('favicon.ico').hash == '202cb962ac59075b964b07152d234b70'

    响应体内容是否包含特定字符：
    lambda g: 'qscms' in g('').text

    响应头中某属性是否等于某个值：
    lambda g: g('').header('X-Powered-By') == 'QSCMS'

    响应体某元素是否等于某值:
    lambda g: g('').html('title') == 'MY_TITLE'
'''
# 处理参数
args = sys.argv[1:]
prop = ""

if len(args) == 0:
    print('使用-h参数查看帮助')
    exit()

for a in args:
    if a[0] == "-":
        if a == "-l":
            prop = "local"
        elif a == "-u":
            prop = "url"
        elif a == "--show-finger":
            for fgr in finger:
                print("| %s | %s"%(fgr['id'], fgr['description']))
            exit()
        elif a == "-i":
            prop = 'cms_id'
        elif a == "-v":
            prop = 'cms_version'
        elif a == "--attack":
            attack = True
        elif a == "-h":
            print(help_str)
            exit()
        else:
            print('无法识别的参数' + a + '，使用-h参数查看帮助')
            exit()
    else:
        if prop == "local":
            # 读取文件
            with open(a, 'r') as f:
                urls = f.readlines()
        elif prop == "url":
            urls = a.split(',')
        elif prop == 'cms_id':
            cms_id = a
        elif prop == 'cms_version':
            cms_version = a
        

print('======================= start ===============================')
result = []
if cms_id:
    for url in urls:
        result.append({'url': url, 'id': cms_id, 'version': cms_version})
else:
    result = start(finger, urls)

if attack and len(result) > 0:
    attack_it(result)
print('======================= finished ============================')